package scau.hotel.hotel_sys.security.filter;


import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import scau.hotel.hotel_model.redis.RedisCache;
import scau.hotel.hotel_model.util.StringUtils;
import scau.hotel.hotel_sys.security.domain.LoginUser;
import scau.hotel.hotel_sys.security.service.TokenService;
import scau.hotel.hotel_sys.security.utils.SecurityUtils;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;


/**
 * 用于验证token的过滤器
 */
@Component("authenticationTokenFilter")
public class MisAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private TokenService tokenService;

    @Resource
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        // 1. 从请求头中获取token
        String token = request.getHeader("Authorization");

        //不存在token予以放行，下一个过滤器会继续过滤
        //客户端允许匿名访问，因此直接放行
        // !!!如果不增加判断条件，白名单url会因为token为空报错，从而导致白名单失效!!!
        if (!org.springframework.util.StringUtils.hasText(token)) {
            //放行
            chain.doFilter(request, response);
            return;
        }

        // 2. 解析token获得claims
        Claims claims = tokenService.parseToken(token);
        
        // 3. 判断token是否过期
        if (tokenService.isTokenExpired(claims)) {
            throw new JwtException("token已过期");
        }
        // 4. 得到subject
        String userName = claims.getSubject();  // 获取token是设置的subject是用户名。注意subject可以是LoginUser的任意唯一字段

        // 5. subject与LoginUser关联

        // TODO 将subject与之前用户认证的loginUser关联，此处需要用到Redis
        String userKey = userName;  // redis缓存需设置唯一值，建议uuid
        LoginUser loginUser = redisCache.getCacheObject(userKey);

        // 5. 如果得到loginUser，使该loginUser进行用户认证
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }

        // 如果认证过期
        //将filter抛出的异常转发到异常controller，使其被全局异常处理器捕获，以返回自定义信息
        try {
            if (loginUser.getExpireTime() < System.currentTimeMillis()) {
                throw new CredentialsExpiredException("用户缓存有效期已过，请重新登录");
            } else {
                // 更新缓存有效期时间
                loginUser.setLoginTime(System.currentTimeMillis());
                int expireTime = 30*60*1000; //设置loginUser的缓存过期时间为30分钟。建议与token过期时间相等或者小于token过期时间。
//                int expireTime = 60 * 1000; //设置loginUser的缓存过期时间为30分钟。建议与token过期时间相等或者小于token过期时间。
                loginUser.setExpireTime(loginUser.getLoginTime() + expireTime);
                redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
            }
        }catch (CredentialsExpiredException e){
            // 异常捕获，发送到credentialsExpiredException
            request.setAttribute("credentialsExpiredException", e);
            //将异常分发到/credentialsExpiredException控制器
            request.getRequestDispatcher("/credentialsExpiredException").forward(request, response);
        }

        // 6. 进入下一个过滤器(即UsernamePasswordAuthenticationFilter)进行用户认证
        chain.doFilter(request, response);
    }
}
